LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

SpringCloud基础+电商实战

2024/4/11 后端

Spring Cloud基础介绍

  • 微服务的基本概念、设计与拆分原则
  • 微服务和Spring Cloud的关系
  • 微服务常见的组件和功能
  • 课程查询案例基本介绍、系统架构设计和接口设计
  • 分模块构建Spring Cloud项目
  • 完成课程列表、课程价格服务开发
  • 课程服务整合,服务注册与发现
  • 整合Feign实现服务间调用
  • 网关的集成与开发,并接入服务
  • 引入服务的熔断与降级,并进行实操演练

微服务基础

  • 什么是微服务?
  • 微服务的特点
  • 微服务优缺点
  • 微服务的两大门派 [Spring Cloud 和 Dubbo]
  • 微服务拆分
  • 微服务扩展
  • 微服务重要模块

微服务热度

单体应用的痛点

什么是服务化
  • 把传统的单机应用中的本地方法调用,改造成通过RPC、HTTP产生的远程方法调用
  • 把模块从单体应用中拆分出来,独立成一个服务部署
什么是微服务
  • 一系列、一部分

  • 是一种架构风格

  • 开发单体应用作为一系列小型服务的套件,其中每个服务都运行再自己的进程中,并且通过轻量级的机制实现彼此间的通信,这通常是HTTP资源API

  • 这些服务是围绕着业务功能构建的,并且可以通过完全自动化的部署机制进行独立部署

  • 这些服务的集中式管理做到了最小化(例如docker相关技术),每一种服务都可以通过不同的编程语言进行编写,并且可以使用不同的数据存储技术

微服务的特点

  • 组件以服务形式来提供

  • 产品不是项目

  • 轻量级通信、独立进程

  • 分散治理、去中心化治理

  • 容错性设计

  • 会带来团队组织架构的调整

微服务优缺点

  • 服务简单、便于学习和上手,相对易于维护
  • 独立部署,灵活扩展
  • 技术栈丰富
微服务缺点
  • 运维成本过高 [磁盘满 CPU高]
  • 接口可能不匹配
  • 代码可能重复 [要非常明确的定义每个API]
  • 架构复杂度提高

微服务两大门派

  • Spring Cloud:众多子项目【最大供应者:Netflix内容付费知识】
  • dubbo:高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
  • dubbo提供的能力只是SpringCloud的一部分子集
[Dubbo虽然有很多不提供的组件 但是可以和其他供应商合作完成提供任务]
核心组件 Dubbo Spring Cloud
服务注册中心 Zookeeper Spring Cloud Netflix Eureka
服务调用方式 RPC REST API
服务网关 无 Spring Cloud Netflix Zuul
断路器 不完善 Spring Cloud Netflix Hystrix
分布式配置 无 Spring Cloud Config
服务跟踪 无 Spring Cloud Sleuth
消息总线 无 Spring Cloud Bus
数据流 无 Spring Cloud Stream
批量任务 无 Spring Cloud Task
通信协议对比
  • RPC vs REST
  • 服务提供方与调用方接口依赖方式太强
  • 服务对平台敏感,难以简单复用
文档质量对比
  • Dubbo的文档可以说再国内开源框架中算是一流的,提供了中文与英文两种版本
  • Spring Cloud文档体量大,更多的是偏向整合,更深的使用方法还是需要查看整合组件的详细文档
两大门派选型建议
  • Dubbo => 组装电脑 【中文文档多】
  • Spring Cloud => 品牌机【稳定可靠】

微服务拆分

什么时候进行服务化拆分
  • 第一阶段的主要目标是快速开发和验证想法
  • 进一步增加更多的新特性来吸引更多的目标用户
  • 同时进行开发的人员超过10人,这个时候就该考虑到服务化拆分了
不适合拆分的情况
  • 小团队,技术基础较薄弱
  • 流量不高,压力小,业务变化也不大
  • 对延迟很敏感的低延迟高并发系统
服务化拆分的两种方式
  • 纵向拆分[上方区别图]
  • 横向拆分
  • 结合业务综合分析

服务扩展

维度

自动按需扩展
  • 根据CPU负载程度、特定时间(比如周末)、消息中间件的队列长度、业务具体规则、预测等来决定是否扩展
  • 自动分配一个新的服务实例,提高可用性
  • 提高了可伸缩性(双11之后,自动减少服务器)
  • 具有最佳使用率,节约成本

微服务重要模块

  • 服务描述
  • 注册中心
  • 服务框架
  • 负载均衡
  • 熔断和降级
  • 网关

Spring Cloud课程查询

  • Spring Cloud简介
  • 项目整体设计
  • 课程列表模块开发
  • 课程价格模块开发 [模块间互相调用]
  • 服务注册与发现Eureka
  • 服务间调用Feign
  • 负载均衡Ribbon
  • 熔断器Hystrix [兜底界面 默认返回]
  • 网关Zuul
  • 整体测试

Spring Cloud简介

  • 成熟的微服务框架,定位为开发人员提供工具,以快速构建分布式系统
核心组件
核心组件 Spring Cloud
服务注册中心 Spring Cloud Netflix Eureka
服务调用方式 REST API、Feign、Ribbon
服务网关 Spring Cloud Netflix Zuul
熔断器 Spring Cloud Netflix Hystrix

项目整体设计

  • 项目介绍

  • 接口设计
    • 课程列表
    • 单个课程价格
    • 整合课程列表和价格
  • 表设计

系统数据流向

【课程列表数据】→ 【课程列表服务】
↓
【课程价格数据】→ 【课程价格服务】→ 【整体列表+价格】

新建多模块项目

创建一个 spring-cloud-course-pracice 并删除src文件夹
新建New → module → Maven → course-service → 删除src
    新建New → module → New Module → Maven → course-list → Parent:course-service 
    新建New → module → New Module → Maven → course-price → Parent:course-service 
D:\Java+4399\阶段5:Java分布与微服务实战\第32周 Spring Cloud基础\第2节 Spring Cloud开发课程查询功能\辅助材料\SpringCloud课程查询源码【优质it资源微信it-wangke18】.zip\SpringCloud课程查询源码\课程价格模块开发后\spring-cloud-course-practice
course-service.course-list
pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-list</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CourseListController.java
package com.imooc.course.controller;

import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * CourseListController课程列表Controller
 */

@RestController
public class CourseListController {
    //提供课程列表服务
    @Autowired //引入课程服务
    CourseListService courseListService;


    @GetMapping("/courses")
    public List<Course> courseList() {
        //return → service
        return courseListService.getCourseList();
    }
}
com/imooc/course/service/CourseListService.java
package com.imooc.course.service;

import com.imooc.course.entity.Course;

import java.util.List;

/**
 * 课程列表服务
 */
public interface CourseListService {
    List<Course> getCourseList();
}
com/imooc/course/service/impl/CourseListServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CourseMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.service.CourseListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 课程服务实现类
 */
@Service
public class CourseListServiceImpl implements CourseListService {
    @Autowired
    CourseMapper courseMapper;
    @Override
    public List<Course> getCourseList() {
        return courseMapper.findValidCourses();
    }
}
com/imooc/course/entity/Course.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * Course的实体类
 */
public class Course implements Serializable {
    Integer id;
    Integer courseId;
    String courseName;
    Integer valid;

    @Override
    public String toString() {
        return "Course{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", courseName='" + courseName + '\'' +
                ", valid=" + valid +
                '}';
    }
    Getter+Setter  
}
com/imooc/course/dao/CourseMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.Course;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 描述课程的Mapper类
 */
@Mapper
@Repository
public interface CourseMapper {
    @Select("SELECT * FROM course WHERE valid = 1")
    List<Course> findValidCourses();
}
application.properties

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity的实体类与数据库名进行驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=====================================================================
数据库名:course_practice
   表名:course
   字段:id[自增]   course_id   course_name   valid
课程列表模块开发-总结 [注意点]
  • 多模块开发
  • 实体类实现Serializable接口、set方法
  • MyBatis的驼峰配置
course-list.course-price
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>course-service</artifactId>
    <groupId>com.imooc</groupId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>course-price</artifactId>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 课程价格控制器
 */

@RestController
public class CoursePriceController {
    @Autowired
    CoursePriceService coursePriceService;
    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId){
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }
}
com/imooc/course/service/CoursePriceService.java
package com.imooc.course.service;

import com.imooc.course.entity.CoursePrice;

import java.util.List;

/**
 * 课程价格服务
 */
public interface CoursePriceService {
    CoursePrice getCoursePrice(Integer courseId);
}
com/imooc/course/service/impl/CoursepriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 课程价格的服务实现类
 */
@Service
public class CoursepriceServiceImpl implements CoursePriceService {
    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }
}
com/imooc/course/entity/CoursePrice.java
package com.imooc.course.entity;

import java.io.Serializable;

/**
 * CoursePrice的实体类
 */
public class CoursePrice implements Serializable {
    Integer id;
    Integer courseId;
    Integer price;

    @Override
    public String toString() {
        return "CoursePrice{" +
                "id=" + id +
                ", courseId=" + courseId +
                ", price=" + price +
                '}';
    }
    Getter+Setter
}
com/imooc/course/dao/CoursePriceMapper.java
package com.imooc.course.dao;

import com.imooc.course.entity.CoursePrice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * 课程价格Mapper类
 */
@Mapper
@Repository
public interface CoursePriceMapper {
    @Select("SELECT * FROM course_price WHERE course_id = #{courseId}")
    CoursePrice findCoursePrice(Integer courseId);
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
==========================================================
http://127.0.0.1:8082/price?courseId=409
数据库名:course_practice
   表名:course_price
   字段:id[自增]   course_id    price

Eureka的作用和架构

Eureka
  • 用于定位服务,直接找到组件中的各个服务地址
  • 114[各种服务的提供者]、物业[维护各个住户的信息(注册中心)]
为什么需要服务注册与发现 [移除不影响 但会有很多麻烦]
  • IP变化
  • 难以维护
  • 改进
    • 节点变化[服务的提供者和消费者] [消费者需要调用提供者的API来获得服务] 若提供者修改了ip 此时应该上传到注册中心,消费者此时需要得到IP和API 无需直接找提供者 直接去注册中心调用
Eureka架构
  • EureKa Server 和 EureKa Client

  • 集群 [只要能获得一个Eureka Server 就能获得整个信息]

引入Eureka

  • 引入依赖
  • 配置文件
  • 启动注解
pom.xml(eureka-server)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-course-practice</artifactId>
        <groupId>com.imooc</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>course-eureka-server</name>
    <description>Spring Cloud Eureka</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml(Spring-cloud-course-practice)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <packaging>pom</packaging>
  <modules>
    <module>course-service</module>
    <module>eureka-server</module>
  </modules>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.imooc</groupId>
  <artifactId>spring-cloud-course-practice</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>spring-cloud-course-practice</name>
  <description>course project for Spring Cloud</description>

  <properties>
    <java.version>1.8</java.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <!--  表示Spring Cloud的版本-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Greenwich.SR5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>
application.properties(eureka-server)

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#fetch-registry???????????????????
eureka.client.fetch-registry=false
#register-with-eureka??????????Eureka Server????true?
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
application.properties(course-list)

server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
# entity?????????????????
mybatis.configuration.map-underscore-to-camel-case=true 
spring.application.name=course-list
#??????? eureka-server??application.properties?defaultZone????????????????
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
application.properties(course-price)

server.port=8082
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/course_practice?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
spring.application.name=course-price
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/EurekaServerApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * Eureka的服务端
 */
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
========================================================
http://127.0.0.1:8000/

利用Feign实现服务间调用

Feign
  • 声明式、模板化的HTTP客户端,方便的调用远程的HTTP请求 [基于接口实现]
集成Feign
  • 引入依赖
  • 配置文件
  • 注解
pom.xml (course-price)
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

application.properties
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
com/imooc/course/controller/CoursePriceController.java
package com.imooc.course.controller;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import java.util.List;
import javax.xml.ws.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:     课程价格控制器
 */
@RestController
public class CoursePriceController {

    @Autowired
    CoursePriceService coursePriceService;

    @Autowired
    CourseListClient courseListClient;

    @GetMapping("/price")
    public Integer getCoursePrice(Integer courseId) {
        CoursePrice coursePrice = coursePriceService.getCoursePrice(courseId);
        return coursePrice.getPrice();
    }

    @GetMapping("/coursesInPrice")
    public List<Course> getCourseListInPrice(Integer courseId) {
        List<Course> courses = courseListClient.courseList();
        return courses;
    }
}
com/imooc/course/client/CourseListClient.java[接口]
package com.imooc.course.client;

import com.imooc.course.entity.Course;
import java.util.List;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 描述:     课程列表的Feign客户端
 */
@FeignClient("course-list")
public interface CourseListClient {

    @GetMapping("/courses")
    List<Course> courseList();
}
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
=============================================================
http://127.0.0.1:8082/coursesInPrice

[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]

负载均衡的两种类型

  • 客户端负载均衡(Ribbon)
  • 服务端负载均衡(Nginx)
负载均衡策略
  • RandomRule 表示随机策略
  • RoundRobinRule 表示轮询策略
  • ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
配置不同的负载均衡方式
  • Ribbon.NFLoadBalancerRuleClassName
application.properties(course-price)
course-list.ribbon.NFLoadBanlancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

为什么要断路器

Hystrix
pom.xml(course-price)   
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
application.properties(course-price)
feign.hystrix.enabled=true
com/imooc/course/CoursePriceApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class CoursePriceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoursePriceApplication.class, args);
    }
}
================================================================
所有服务都打开时 http://127.0.0.1:8082/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud白学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
当把CouresListApplication服务关闭的时候
[
    {
        "id": 1,
        "courseId": 1,
        "courseName": "默认课程",
        "valid": 1
    }
]

整合两个服务

com/imooc/course/controller/CoursePriceController.java
    @GetMapping("/coursesAndPrice")
    public List<CourseAndPrice> getCoursesAndPrice(){
        List<CourseAndPrice> courseAndPrices = coursePriceService.getCourseAndPrice();
        return courseAndPrices;
    }
=====================================================
http://127.0.0.1:8082/coursesAndPrice
[
    {
        "id": 1,
        "courseId": 362,
        "name": "SpringCloud自学",
        "price": 348
    },
    {
        "id": 2,
        "courseId": 409,
        "name": "玩转Java并发工具",
        "price": 399
    }
]
com/imooc/course/service/CoursePriceService.java
public interface CoursePriceService {

    CoursePrice getCoursePrice(Integer courseId);
    List<CourseAndPrice> getCourseAndPrice();
}
com/imooc/course/service/impl/CoursePriceServiceImpl.java
package com.imooc.course.service.impl;

import com.imooc.course.client.CourseListClient;
import com.imooc.course.dao.CoursePriceMapper;
import com.imooc.course.entity.Course;
import com.imooc.course.entity.CourseAndPrice;
import com.imooc.course.entity.CoursePrice;
import com.imooc.course.service.CoursePriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:     课程价格的服务实现类
 */
@Service
public class CoursePriceServiceImpl implements CoursePriceService {

    @Autowired
    CoursePriceMapper coursePriceMapper;
    @Autowired
    CourseListClient courseListClient;

    @Override
    public CoursePrice getCoursePrice(Integer courseId) {
        return coursePriceMapper.findCoursePrice(courseId);
    }

    @Override
    public List<CourseAndPrice> getCourseAndPrice() {
        List<CourseAndPrice> courseAndPrices = new ArrayList<>();
        List<Course> courses = courseListClient.courseList();
        for (int i = 0; i < courses.size(); i++) {
            Course course = courses.get(i);
            //inn
            if (course != null) {
                CoursePrice coursePrice = getCoursePrice(course.getCourseId());
                CourseAndPrice courseAndPrice = new CourseAndPrice();
                courseAndPrice.setPrice(coursePrice.getPrice());
                courseAndPrice.setName(course.getCourseName());
                courseAndPrice.setId(coursePrice.getId());
                courseAndPrice.setCourseId(course.getCourseId());
                courseAndPrices.add(courseAndPrice);
            }
        }
        return courseAndPrices;
    }
}
com/imooc/course/entity/CourseAndPrice.java
package com.imooc.course.entity;

/**
 * 课程与价格的融合类
 */
public class CourseAndPrice {
    Integer id;
    Integer courseId;
    String name;
    Integer price;
}

网关Zuul

  • 为什么需要网关
  • 签名校验、登录校验冗余问题
  • Spring Cloud Zuul 与 Spring Cloud
  • API网关允许您将API请求(内部或外部)路由到正确的位置

集成Zuul [统一修改访问url地址]

  • 把自己注册到Eureka这个注册中心
  • 引入依赖
  • 配置路由地址
pom.xml(course-zuul)
     <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>
com/imooc/course/ZuulGatewayApplication.java
package com.imooc.course;

import org.springframework.boot.SpringApplication;

/**
 * 网关启动类
 */
@EnableZuulProxy
@SpringCloudApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
===============================================================
http://127.0.0.1:9000/imooc/price/coursesInPrice
[
    {
        "id": 1,
        "courseId": 362,
        "courseName": "SpringCloud自学",
        "valid": 1
    },
    {
        "id": 2,
        "courseId": 409,
        "courseName": "玩转Java并发工具",
        "valid": 1
    }
]
application.properties

spring.application.name=course-gateway
server.port=9000
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
mybatis.configuration.map-underscore-to-camel-case=true
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

zuul.prefix=/imooc
zuul.routes.course-list.path=/list/**
zuul.routes.course-list.service-id=course-list
zuul.routes.course-price.path=/price/**
zuul.routes.course-price.service-id=course-price

利用网关实现过滤器

  • pre 过滤器在路由请求之前运行
  • route 过滤器可以处理请求的实际路由
  • post 路由请求后运行过滤器
  • error 如果在处理请求的过程中发生错误,则过滤器将运行
com/imooc/course/filter/PreRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 记录请求时间
 */
@Component
public class PreRequestFilter extends ZuulFilter {

    @Override
    public String filterType() {
        //过滤器的类型
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        //是否启用过滤器
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //通过时间戳 获取上下文
        RequestContext currentContext = RequestContext.getCurrentContext();
        currentContext.set("startTime",System.currentTimeMillis());
        System.out.println("过滤器已经记录时间");
        return null;
    }
}
com/imooc/course/filter/PostRequestFilter.java
package com.imooc.course.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * 请求处理后的过滤器
 */
@Component //有了才能被spring捕捉到
public class PostRequestFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() { //在其之前-1 先运行
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        Long startTime = (Long) currentContext.get("startTime");
        long duration = System.currentTimeMillis() - startTime;
        String requestURI = currentContext.getRequest().getRequestURI();
        System.out.println("uri:" + requestURI + ",处理时长:" + duration);
        return null;
    }
}


Spring Cloud电商实践

  • 服务拆分过程分析和经验分享
  • 完成用户、商品、购物车和订单等服务开发
  • 通用common模块的拆分和应用
  • Eureka server注册中心开发,用Feign完成服务之间的调用
  • 共享Session的处理方案
  • 统一网关的集成与开发

Spring Cloud电商项目

  • 项目介绍
    • 在Spring Boot的基础上升级为Spring Cloud
    • 从0到1 (Spring Boot)
    • 从1到多、微服务
  • 模块拆分
  • Eureka-server开发
  • 用户模块开发
  • 公共模块开发
  • 网关模块开发
  • 商品分类和商品模块开发
  • 购物车和订单模块开发
  • 总结

模块拆分

  • 粒度:过粗、适中、过细

  • 人员的角度

  • 业务的角度:相关、独立

  • Eureka-server模块

  • 网关模块

  • 公共模块 (md5、统一API返回、常量、异常、异常枚举、二维码、工具类模块……)

  • 用户模块 (相对来说比较独立,登录的时候无需获得商品信息)

  • 商品分类和商品模块

  • 购物车和订单模块

功能模块介绍

  • 项目功能:

    • 前台  {用户、商品分类、商品信息、购物车、订单}
      • 用户模块{注册、登录、更新签名、身份认证、登出}
      • 商品分类模块{多级目录、递归查询、缓存}
      • 商品模块{商品搜索、商品排序、商品列表、目录展示、商品详情}
      • 购物车模块{加入商品、列表显示、数量更改、删除商品、勾选反选、全选全不选}
      • 订单模块{下单、订单流程、订单详情、取消订单、支付二维码、扫码支付、个人订单、确认收货}
    • 后台  {用户、商品分类、商品信息、订单}
      • 管理员模块{登录登出、身份认证、安全限制}
      • 商品分类模块{分类列表、增加分类、修改分类、删除分类}
      • 商品模块{商品列表、新增商品、图片上传、更新删除、批量上下架}
      • 订单模块{订单列表、地址信息、发货、订单完结}

``

项目初始化

Eureka-server模块开发
  • 引入依赖
  • 配置文件
  • 启动注解

由于是多模块开发,创建了maven项目后把cloud-mall-practice的src删除

pom.xml(cloud-mall-practice)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.imooc</groupId>
    <artifactId>cloud-mall-practice</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>cloud-mall-eureka-server</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
cloud-mall-eureka-server/com/imooc/cloud/mall/practice/eureka/EurekaServerApplication.java
package com.imooc.cloud.mall.practice.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * 1. Eureka Server的启动类,提供服务注册与发现
 * 2写其pom和resources的配置文件
 *
 */
@EnableEurekaServer //对外提供服务
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
pom.xml(cloud-mall-eureka-server)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-eureka-server</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <name>
        cloud-mall-eureka-server
    </name>
    <description>Spring cloud Eureka Server</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties

spring.application.name=eureka-server
server.port=8000
eureka.instance.hostname=localhost
#是否同步其他节点的信息
eureka.client.fetch-registry=false
#是否把自己作为服务注册在服务上
eureka.client.register-with-eureka=false
#eureka-server所在的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

用户模块知识点

登录、注册[加盐md5]、重名校验[注册就不可注册]、密码加密存储、Session的使用、越权校验[不可编辑别人的签名]

用户模块
  • 表设计
  • 开发
  • 测试

idea中如何将包名折叠或者或如何将折叠的包名展开_idea包名折叠-CSDN博客

用户模块初始化

公共模块
  • 常量、异常、工具类
  • 自身不是Spring Boot项目
进行模块各层级的重构 + 用户模块的测试
一定要记得如果引用其他muder的时候 要在xml中假如其项目文件的依赖才可以跨项目引用
比如我这个项目是【一定要引用噢!! 不然在非其项目的时候找不到import导包】
pom.xml(cloud-mall-zuul)
           <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>

网关模块开发

pom.xml(cloud-mall-zuul)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.imooc</groupId>
        <artifactId>cloud-mall-practice</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloud-mall-zuul</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
application.properties(cloud-mall-zuul)
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
com/imooc/cloud/mall/practice/zuul/filter/UserFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 用户鉴权过滤器
 */
@Component
public class UserFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("images") || requestURI.contains("pay")) {
            return false;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10007,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/filter/AdminFilter.java
package com.imooc.cloud.mall.practice.zuul.filter;

import com.imooc.cloud.mall.practice.common.common.Constant;
import com.imooc.cloud.mall.practice.user.model.pojo.User;
import com.imooc.cloud.mall.practice.zuul.feign.UserFeignClient;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * 管理员鉴权过滤器
 */
@Component
public class AdminFilter extends ZuulFilter {
    @Autowired
    UserFeignClient userFeignClient;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String requestURI = request.getRequestURI();
        //不经过过滤器
        if (requestURI.contains("adminLogin")){
            return false;
        }
        if (requestURI.contains("admin")){
            return true;
        }
        if (requestURI.contains("cart") || requestURI.contains("order")) {//前置条件必须要登录!
            return true;
        }
        return false;
    }

    @Override
    //用户过滤器
    public Object run() throws ZuulException {//返回true的时候执行的
        RequestContext currentContext = RequestContext.getCurrentContext();//获取request
        //获取session 因为user对象保存在里面  session.getAttribute取出session对象
        HttpServletRequest request = currentContext.getRequest();
        HttpSession session = request.getSession();
        //拿出User对象
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        if (currentUser == null) {
            //无需通过网关再去发送
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10010,\n"
                    + "    \"msg\": \"NEED_LOGIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
            return null; //程序可以停止了
        }
        //进一步判断是否是管理员
        Boolean adminRole = userFeignClient.checkAdminRole(currentUser);
        if (!adminRole){
            currentContext.setSendZuulResponse(false);
            //返回给前端的对象
            currentContext.setResponseBody("{\n"
                    + "    \"status\": 10011,\n"
                    + "    \"msg\": \"NEED_ADMIN\",\n"
                    + "    \"data\": null\n"
                    + "}");
            currentContext.setResponseStatusCode(200);
        }
        return null;
    }
}
com/imooc/cloud/mall/practice/zuul/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.zuul.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * UserFeignClient
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    @PostMapping("/checkAdminRole")
    public Boolean checkAdminRole(@RequestBody User user);
}
com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
网关模块加上SpringBoot启动类

package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}
=================================================================
127.0.0.1:8083/user/login?userName=mumu9&password=lihao123
{
    "status": 10000,
    "msg": "SUCCESS",
    "data": {
        "id": 21,
        "username": "mumu9",
        "password": null,
        "personalizedSignature": "乘风破浪的姐姐",
        "role": 2,
        "createTime": "2024-04-17T18:08:15.000+0000",
        "updateTime": "2024-04-17T18:10:49.000+0000"
    }
}

POSTMAN中cloud-mall-practice的注册新用户、用户登录、管理员登录、登出模块都通过网关共用地址的调用,但是更新个性签名不可以,提示需要登录,因为更新签名需要提前登录。但是问题的最主要的点就是没有拿到session所以并没有把用户信息传到个性签名中 这是就需要Session共享机制

Session共享机制

登录功能分析
  • 登录状态需要保持
  • session的实现方案:登陆后,会保存用户信息到session
  • 之后的访问,先从session中获取用户信息,然后再执行业务逻辑
目前遇到的障碍 [记得去电脑端启动Redis]
  • session被网关过滤
  • 共享session [现在是多模块项目 其他项目保存了session 另一个项目无法获得]
  • EnableRedisHttpSession[需要通过中介Redis去调取]
在cloud-mall-user/application.properties中
server.port=8081

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-user

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
在cloud-mall-zuul/application.properties中
server.port=8083

spring.datasource.name=imooc_mall_datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/imooc_mall?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath*:mappers/*.xml

logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/

spring.application.name=cloud-mall-zuul

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=

#没有敏感的需要过滤
zuul.sensitive-headers=
zuul.host.connect-timeout-millis=15000
zuul.prefix=/
#凡是用户模块都要走/user地址
zuul.routes.cloud-mall-user.path=/user/**
#模块的名字
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
之后在UserApplication和ZuulGatewayApplication前面加上@EnableRedisHttpSession

com/imooc/cloud/mall/practice/user/UserApplication.java
package com.imooc.cloud.mall.practice.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * 启动类
 */
@SpringBootApplication
@EnableSwagger2
@MapperScan(basePackages = "com.imooc.cloud.mall.practice.user.model.dao")
@EnableRedisHttpSession
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

-------------------------------------------------------------------------------------

com/imooc/cloud/mall/practice/zuul/ZuulGatewayApplication.java
package com.imooc.cloud.mall.practice.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

/**
 * 网关启动类
 */
@EnableZuulProxy
@EnableFeignClients
@SpringBootApplication
@EnableRedisHttpSession
public class ZuulGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulGatewayApplication.class, args);
    }
}

商品分类与商品模块初始化

什么是商品分类
  • 条理清楚,层次分明
  • 方便用户进行筛选和辨别
  • 可以通过分类的设置快速的进入对应的商品列表页面进行商品选择
分类层级
  • 在商品分类上需要继续做归类操作
  • 分类设置成三级
  • 层级太深的弊端:
    • 一是对用户不太友好,不利于寻找
    • 二是对于后台管理人员不友好,不方便管理

[cloud-mall-category-product]创造出来,将controller,model(dao,pojo),service,impi 里转入 Category来搞 因为在网关项目里写了关于管理员校验的方法 所以在此次处CategoryController把session里的校验删除

要让商品和商品目录用到User类 一定要去pom文件里添加依赖 不然idea找不到
pom.xml(cloud-mall-category-product)
        <dependency>
            <groupId>com.imooc</groupId>
            <artifactId>cloud-mall-user</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
com/imooc/cloud/mall/practice/categoryproduct/config/CachingConfig.java
创建处理缓存的配置类 configpackage com.imooc.cloud.mall.practice.categoryproduct.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

*/**
* ** 57.缓存的配置类 想要运行成功保存序列化 要去弄个序列化接口
*   CategoryVO implements Serializable
* **/
*@Configuration
@EnableCaching
public class  CachingConfig {
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheWriter redisCacheWriter = RedisCacheWriter
                .lockingRedisCacheWriter(connectionFactory);
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                cacheConfiguration);
        return redisCacheManager;
    }
}
在网关中增加path和service-id
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user

zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
查端口  => netstat -ano | findstr :8083 [pid为7812]
杀死端口PID => taskkill /PID 7812 /f

商品模块

更新和新增商品
  • 合并写法不可取
  • 业务逻辑清晰、独立
批量上下架
  • MyBatis遍历List
  • where语句拼接
商品列表:搜索功能
入参判空 → 加%通配符 → like关键字
对于查询目录的in处理
  • 目录处理:如果查某个目录下的商品,不仅是需要查出来该目录的,还需要查出来子目录的所有商品
  • 这里要拿到某一个目录Id下的所有子目录id的List

前台:商品列表

  • 排序功能
  • Mybatis PageHelper
  • 枚举:order by
把所有product的java都移动过去 controller/service/impl/modal.dao/modal.pojo/query.ProductListQuery/request.AddProductReq、ProductListReq、UpdateProductReq/resourcs.mappers.ProductMapper.xml
将Constant里的@Value("${file.upload.dir}")的文件上传目录重构到category-product项目中
然后在其项目中的resources的application.properties中写一下路径
文件地址映射:config/imoocMallWebMvcConfig

图片端口的特殊处理

application.properties(cloud-mall-category-product)
file.upload.dir=/Users/Pluminary/Desktop/idea_Space/imooc-mall-prepare-static/

不能再通过获取getHost来配置了 根据实际情况去配置 没有办法获取到网关对外暴露的真正端口号
 private URI getHost(URI uri){
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(),null,null,null);
        } catch (URISyntaxException e) {
            effectiveURI = null; //如果新建失败 就返回回去
        }
        return effectiveURI;
    }

要改为com/imooc/cloud/mall/practice/categoryproduct/controller/ProductAdminController.java
/**
 * 58.后台商品管理Controller  pojo的product复制一份到request变成AddProductReq  59.需要ProductService.java
 */
@RestController
public class ProductAdminController {

    @Autowired
    ProductService productService;

    @Value("${file.upload.ip}")
    String ip;

    @Value("${file.upload.port}")
    Integer port;

    @PostMapping("/admin/product/add")
    public ApiRestResponse addProduct(@Valid @RequestBody AddProductReq addProductReq) {
        productService.add(addProductReq);
        return ApiRestResponse.success();
    }

    @PostMapping("/admin/upload/file")
    public ApiRestResponse upload(HttpServletRequest httpServletRequest,
                                  @RequestParam("file") MultipartFile file) {
        String fileName = file.getOriginalFilename();
        String suffixName = fileName.substring(fileName.lastIndexOf("."));
        //生成文件名称UUID
        UUID uuid = UUID.randomUUID();
        String newFileName = uuid.toString() + suffixName;
        //创建文件
        File fileDirectory = new File(ProductConstant.FILE_UPLOAD_DIR);
        File destFile = new File(ProductConstant.FILE_UPLOAD_DIR + newFileName);
        if (!fileDirectory.exists()) {
            if (!fileDirectory.mkdir()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.MKDIR_FAILED);
            }
        }
        try {
            file.transferTo(destFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            return ApiRestResponse
                    .success(getHost(new URI(httpServletRequest.getRequestURL() + "")) + "/category-product/images/"
                            + newFileName);
        } catch (URISyntaxException e) {
            return ApiRestResponse.error(ImoocMallExceptionEnum.UPLOAD_FAILED);
        }
    }

    private URI getHost(URI uri) {
        URI effectiveURI;
        try {
            effectiveURI = new URI(uri.getScheme(), uri.getUserInfo(), ip, port,
                    null, null, null);
        } catch (URISyntaxException e) {
            effectiveURI = null;
        }
        return effectiveURI;
    }

阶段性重难点和常见错误

阶段总结
  • 重难点:模块拆分设计、公共模块、Zuul(网关)过滤器、Session处理、Feign调用
    Session在微服务的情况下就不容易处理到了可以共享Session,把其放在Redis中实现共享
    Feign模块之间的接口调用[HTTP是手动调用]
  • 常见错误:模块粒度不合适、无公共模块、各接口独立校验、session无法共享、HTTP手动调用
模块拆分
  • 粒度:过粗、适中、过细
  • 人员的角度
  • 业务的角度:相关、独立

购物车与订单模块

购物车模块
  • 添加商品到购物车 → 商品是否在售、是否有库存

    • →[否] 提示用户

    • →[是] 该商品之前就在购物车里

      • →[否] 添加新商品
      • →[是] 原有基础上添加数量
创建一个module 加入关于cart的controller/model.dao.pojo.vo/service.impl
其中ProductMapper productMapper会爆红是理所应当的 这样证明耦合不是很严重
利用远程调用Feign进行调用
思路:通过调查发现productMapper只用于挑选ID
Product product = productMapper.selectByPrimaryKey(productId);
所以可以去重构一下代码 回到商品模块的地方cloud-mall-category-product
其中controller中的ProductController
//com/imooc/cloud/mall/practice/categoryproduct/controller/ProductController.java
//这个是服务与服务之间的内部调用 不需要层层包装 只需要返回就好
    @GetMapping("product/detailForFeign")
    public Product detailForFeign(@RequestParam Integer id){
       Product product = productService.detail(id);
       return product;
    }
//千万不要直接引用另一个项目的mapper 因为耦合有点严重 只要对面发生变化 就完蛋了
//com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.categoryproduct.model.pojo.Product;
import org.springframework.web.bind.annotation.RequestParam;

public interface ProductFeignClient {
    Product detailForFeign(@RequestParam Integer id);
}

用户模块提供获取当前用户接口

com/imooc/cloud/mall/practice/user/controller/UserController.java
 @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    } /**
     * 获取当前登录的User对象 为了避免暴露用户信息,可以再filter中对getUser进行拦截
     * @param session
     * @return
     */
    @GetMapping("/getUser")
    @ResponseBody
    public User getUser(HttpSession session){
        User currentUser = (User) session.getAttribute(Constant.IMOOC_MALL_USER);
        return currentUser;
    }
com/imooc/cloud/mall/practice/cartorder/feign/UserFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.user.model.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * UserFeign客户端
 */
@FeignClient(value = "cloud-mall-user")
public interface UserFeignClient {
    /**
     * 获取当前登录的user对象
     */
    @GetMapping("/getUser")
    User getUser();
}
修改CaetMapper.xml的路径名
com/imooc/cloud/mall/practice/cartorder/feign/ProductFeignClient.java
package com.imooc.cloud.mall.practice.cartorder.feign;

import com.imooc.cloud.mall.practice.cartorder.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * 描述:     商品FeignClient
 */
@FeignClient(value = "cloud-mall-category-product")
public interface ProductFeignClient {
    //不经过网关 只是内部调用
    @GetMapping("product/detailForFeign")
    Product detailForFeign(@RequestParam Integer id);

    @PostMapping("product/updateStock")
    void updateStock(@RequestParam Integer productId, @RequestParam Integer stock);

}
将新增模块加入到网关[前缀地址] zuul
zuul.routes.cloud-mall-user.path=/user/**
zuul.routes.cloud-mall-user.service-id=cloud-mall-user
zuul.routes.cloud-mall-category-product.path=/category-product/**
zuul.routes.cloud-mall-category-product.service-id=cloud-mall-category-product
zuul.routes.cloud-mall-cart-order.path=/cart-order/**
zuul.routes.cloud-mall-cart-order.service-id=cloud-mall-cart-order

让Feign携带Session信息

错误新消息:500 Internal Server Error
在购物车中去调用User的getUser方法 是经过Feign
但是Feign的调用不经过网关 它是一个HTTP的调用
需要携带网关的Session信息

  • FeignRequestInterceptor [对每一个发出的Feign进行拦截]
    把网关的所有信息都复制到Feign的请求上 就不会遗漏相关信息
com/imooc/cloud/mall/practice/cartorder/filter/FeignRequestInterceptor.java
package com.imooc.cloud.mall.practice.cartorder.filter;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * 描述:     Feign请求拦截器
 */
@EnableFeignClients
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //通过RequestContextHolder获取到请求 拿到requestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return;
        }//类型转换 拿到Request
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        Enumeration<String> headerNames = request.getHeaderNames(); //拿到所有Header名字
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) { //如果还有新的元素 先获取
                String name = headerNames.nextElement(); //获取相关的值
                Enumeration<String> values = request.getHeaders(name);
                while (values.hasMoreElements()) {//获取到当前元素
                    String value = values.nextElement();
                    requestTemplate.header(name, value);
                }
            }
        }
    }
}

订单模块

  • 表设计和接口设计
"订单编号的名字也要改 不能是以前的自增id 要变成order_no"
order_no是每一种商品的id
否则黑客早上下一单 晚上下一单 相减 就可以估计客流量 
分成了很详细的字段名
product_id/name/img 都是之前购买过的订单产生的数据

生成订单–用户下单

  • 入参
  • 从购物车中查询已经勾选的商品
  • 判断商品是否正在售卖中
  • 判断库存,保证不超卖
  • 调用商品服务扣库存 [及时更新库存]
  • 删除购物车为中对应的商品
  • 生成订单
  • 订单号生成规则
  • 循环保存每一个商品到order_item表

图片路径配置

com/imooc/cloud/mall/practice/cartorder/config/ImoocMallWebMvcConfig.java
package com.imooc.cloud.mall.practice.cartorder.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 描述:     地址映射配置
 */
@Configuration
public class ImoocMallWebMvcConfig implements WebMvcConfigurer {

    @Value("${file.upload.dir}")
    String FILE_UPLOAD_DIR;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**").addResourceLocations("file:" + FILE_UPLOAD_DIR);
    }

购物车和订单模块-重难点

  • 订单表[多个关联(订单编号+状态)]、订单状态设计
  • 购物车流程
  • 下单流程
  • Feign调用的处理
购物车和订单模块-常见错误
  • Feign调用取不到User对象
  • URL错误拦截(图片url可以不拦截[订单+购物车])
  • 路由配置错误(二维码/图片不显示)